/**
 * 
 * @Project HRL Simulation
 * @Date 20.06.2011
 * @Author Aebi Pascal, Kocher Marc, Sriranjan George
 * @School BFH-TI Biel, Projekt 1
 *
 */

package main;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;

import strategy.Strategy;
import strategy.strategies.OptimizedStrategy;
import strategy.strategies.RandomStrategy;
import strategy.strategies.SuccessiveLeftRightStrategy;
import strategy.strategies.SuccessiveTopDownStrategy;
import utils.AgendaEntry;
import utils.Position;
import utils.Prop;
import utils.Resource;
import utils.StatisticsManager;
import elements.Buffer;
import elements.Conveyor;
import elements.Lift;
import elements.Load;
import elements.Rack;
import exceptions.LoadListException;
import exceptions.ResourceException;


public class ControlCenter {
	
	private int RACK_NUMBER;
	private int RACK_LENGTH;
	private int RACK_HEIGHT;
	private int LIFT_INIT_POSITION;
	private Strategy strategy;
	private Conveyor con;
	private List<Lift> lifts = new ArrayList<Lift>();
	private List<Rack> racks = new ArrayList<Rack>();
	private List<Buffer> buffers = new ArrayList<Buffer>();
	private List<Load> alLoadsToStore = new ArrayList<Load>();
	private List<Load> alSortedLoads = new ArrayList<Load>();
	private List<Load> alGeneratedLoads = new ArrayList<Load>();
	private List<Load> alScheduledLoads = new ArrayList<Load>();
	
	
	/**
	 * Create a new control center. The control center manages everything around the high rack storage
	 */
	public ControlCenter(){
		this.RACK_NUMBER = Prop.getInt(Prop.LIFT_NUMBER)*2;
		this.RACK_HEIGHT = Prop.getInt(Prop.RACK_HEIGHT);
		this.RACK_LENGTH = Prop.getInt(Prop.RACK_LENGTH);
		this.LIFT_INIT_POSITION = Prop.getInt(Prop.LIFT_INIT_POSITION);
		
		this.init();
	}
	
	
	/**
	 * Create the conveyor, the buffers, the racks and the lifts
	 */
	private void init(){
		// Conveyor
		this.con = new Conveyor(this.RACK_NUMBER,this.RACK_LENGTH);
		
		// Entry buffer
		this.buffers.add(new Buffer(new Position(0, (4+this.RACK_LENGTH)/2-1, 0), Buffer.Type.IN)); 
		
		// Uupper buffers
		Position pos = new Position(2, 1, 0);
		for(int i=0; i<this.RACK_NUMBER; i++){
			if(i%2 == 0){
				this.buffers.add(new Buffer(new Position(pos.getX(), pos.getY(), pos.getZ()), Buffer.Type.IN));
				this.buffers.add(new Buffer(new Position(pos.getX()+2, pos.getY(), pos.getZ()), Buffer.Type.OUT));
				pos = new Position(pos.getX()+3, pos.getY(), pos.getZ());
			}
		}
		
		// Lower buffers
		pos = new Position(pos.getX()-1, 2+this.RACK_LENGTH, 0);
		for(int i=0; i<this.RACK_NUMBER; i++){
			if(i%2 == 0){
				this.buffers.add(new Buffer(new Position(pos.getX(), pos.getY(), pos.getZ()), Buffer.Type.IN));
				this.buffers.add(new Buffer(new Position(pos.getX()-2, pos.getY(), pos.getZ()), Buffer.Type.OUT));
				pos = new Position(pos.getX()-3, pos.getY(), pos.getZ());
			}
		}
		
		// Exit buffer
		this.buffers.add(new Buffer(new Position(0, (4+this.RACK_LENGTH)/2, pos.getZ()), Buffer.Type.OUT)); 
		
		// All racks
		pos = new Position(2, 2, 0);
		for(int i=0; i<this.RACK_NUMBER; i++){
			this.racks.add(new Rack(pos, this.RACK_LENGTH, this.RACK_HEIGHT));
			if(i%2 == 0){
				pos = new Position(pos.getX()+2, pos.getY(), pos.getZ());
			}else{
				pos = new Position(pos.getX()+1, pos.getY(), pos.getZ());
			}
		}
		
		// Start position of lifts
		int liftPosY = (4+this.RACK_LENGTH)/2; //middle
		
		// Top
		if(LIFT_INIT_POSITION == 0){
			liftPosY = 1;
		}
		// Middle
		else if (LIFT_INIT_POSITION == 1) {
			liftPosY = (4+this.RACK_LENGTH)/2;
		}
		// Bottom 
		else if (LIFT_INIT_POSITION == 2) {
			liftPosY = 2+this.RACK_LENGTH;
		}
		
		// All lifts
		pos = new Position(3, liftPosY, 0);
		for(int i=0; i<this.RACK_NUMBER/2; i++){
			this.lifts.add(new Lift(pos));
			pos = new Position(pos.getX()+3, pos.getY(), pos.getZ());
		}
	}


	/**
	 * Initialize the racks.
	 * If there are some loads which have to be in the system from beginning of the simulation,
	 * the corresponding agenda entries will be created.
	 * 
	 * @throws ResourceException Resource position not found or resource already busy
	 */
	private void initRacks() throws ResourceException {
		System.out.println("stored >--");
		
		Random r = new Random();
		List<Resource> freeRackResources = new ArrayList<Resource>();
		
		for (Load load : this.alLoadsToStore) {
			for (Rack rack : this.racks) {
				freeRackResources.addAll(rack.getFreeResources(0));
			}
			if(freeRackResources.isEmpty()){
				System.err.println("All racks are full, load could not be stored!");
			}
			else{
				Resource resource = freeRackResources.get(r.nextInt(freeRackResources.size()));
				freeRackResources.clear();
				AgendaEntry rackAE = new AgendaEntry(0, resource.getPosition(), Integer.MAX_VALUE-1, resource.getPosition(), load);
				resource.getAgenda().addEntry(rackAE);
				System.out.println(rackAE + "  |  " + resource);
				load.getAgenda().addEntry(rackAE);
				System.out.println(rackAE + "  |  " + load);				
				load.setState(Load.STATE.OUT);
			}	
		}
	}


	/**
	 * Eliminates all double entries and splits up the entries into two lists.
	 * One is a sorted list of all in and out entries the other one is list with all entries to store.
	 * 
	 * @param loads List of loads
	 */
	private void prepareLoadLists(List<Load> loads) throws LoadListException{
		List<Load> inAndOutLoads = new ArrayList<Load>();
	
		Collections.sort(loads);
		
		for(Load load1: loads){
			for(Load load2: loads){
				if(load1.equals(load2) && load1.getState() == load2.getState() && load1 != load2){
					throw new LoadListException("adding same load with same state in list is not possible \nload1: "+load1+"\nload2: "+load2);
				}
				if(load1.getState() == Load.STATE.STORE && load1.getEntryTime() != 0){
					throw new LoadListException("for a stored item entry time must be 0\nload: "+load1);
				}
				if(load1.getState() == Load.STATE.EXIT){
					throw new LoadListException("exit state not possible for entry-list of loads\nload: "+load1);
				}
			}
		}
		
		// Add all storing loads
		for(Load load: loads){
			if(load.getState() == Load.STATE.STORE){
				this.alLoadsToStore.add(load);
			}else{
				inAndOutLoads.add(load);
			}
		}

		// Check in and out for there correctness and exclude if necessary
		boolean isAddable = true;
		for(Load load1 : inAndOutLoads){
			for(Load load2: loads){
				
				if(load1.getState() == Load.STATE.IN){
					//load1 has state IN
					if(!this.alSortedLoads.contains(load1)){
						isAddable = true;
						break;
					}
					
				}else{
					// Load1 has state OUT
					// Earlier load was in or stored and time is after, then it is addable
					if(load1.equals(load2) && load1 !=load2 && load1.getEntryTime()>load2.getEntryTime() && (load2.getState() == Load.STATE.IN || load2.getState() == Load.STATE.STORE)){
						isAddable  = true;
						break;
					}
				}
				isAddable = false;
			}
			// Add this load
			if(isAddable){
				this.alSortedLoads.add(load1);
			}else{
				throw new LoadListException("couldn't add load: "+load1);
			}
		}
	}
	
	
	/**
	 * Create the random strategy 
	 */
	public void createRandomStrategy(){
		this.strategy = new RandomStrategy(this.racks, this.buffers, this.lifts, this.con);
	}

	
	/**
	 * Create the successive left right strategy 
	 */
	public void createSuccessiveLeftRightStrategy(){
		this.strategy = new SuccessiveLeftRightStrategy(this.racks, this.buffers, this.lifts, this.con);
	}
	
	
	/**
	 * Create the successive top down strategy 
	 */
	public void createSuccessiveTopDownStrategy(){
		this.strategy = new SuccessiveTopDownStrategy(this.racks, this.buffers, this.lifts, this.con);
	}
	
	
	/**
	 * Create the optimized strategy 
	 */
	public void createOptimizedStrategy(){
		this.strategy = new OptimizedStrategy(this.racks, this.buffers, this.lifts, this.con);
	}
	
	
	/**
	 * Schedule the loads in the system
	 * 
	 * @throws LoadListException The load list is corrupted or contains false entries
	 * @throws ResourceException Resource position not found or resource already busy
	 */
	public void schedule() throws LoadListException, ResourceException{
		// Sort all loads
		this.prepareLoadLists(this.alGeneratedLoads);
		
		// Store loads
		if(!this.alLoadsToStore.isEmpty()){
			this.initRacks();
		}
		
		this.alScheduledLoads.addAll(this.alLoadsToStore);
		
		// Schedule loads
		for (Load load : this.alSortedLoads) {
			if(load.getState() == Load.STATE.IN){
				System.out.println("\nBook in --> \n");
				this.strategy.bookLoadIn(load);
				this.strategy.bookLoadRoute(load);
				//Set state of load to stored
				load.setState(Load.STATE.STORE);
				this.alScheduledLoads.add(load);
			}else if(load.getState() == Load.STATE.OUT){
				System.out.println("\nBook out <-- \n");
				
				for (Load l : this.alScheduledLoads) {
					if(load.equals(l)){
						int startTime = load.getEntryTime();
						load = l;
						load.setEntryTime(startTime);
						load.setState(Load.STATE.OUT);
						break;
					}
				}
				this.strategy.bookLoadOut(load);
				this.strategy.bookLoadRoute(load);
				//Set state of load to exit
				load.setState(Load.STATE.EXIT);
			}else{
				System.err.println("not supported workflow load: " + load.getState());
			}
		}
	}
	
	
	/**
	 * Generate random loads
	 * 
	 * @param loads Number of loads to generate
	 * @param in If true generate in loads
	 * @param out If true generate out loads
	 * @param store If true generate store loads
	 * @throws LoadListException It is not possible to generate only outgoing loads
	 */
	public void generateRandomLoads(int loads, boolean in, boolean out, boolean store) throws LoadListException{
		List<Load> inAndStoreLoads = new ArrayList<Load>();
		Random r = new Random();
		Load load;
		int booleanSum = 0;
		double inQuotient = 0.70;
		double storeQuotient = 1-inQuotient;
		double outQuotient = 0.35;
		int inLoads = (int) Math.round(loads*inQuotient);
		int storeLoads = (int) Math.round(loads*storeQuotient);
		int outLoads = (int) Math.round(loads*outQuotient);
		
		if(in){
			booleanSum += 1;
		}
		if(out){
			booleanSum += 2;
		}
		if(store){
			booleanSum += 4;
		}
		
		if(loads != 0){
			// Create ONLY input and store loads	-> output loads are created after switch case!
			switch(booleanSum){
				// All booleans are false
				case 0:
					for(int i=0; i<inLoads; i++){
						load = new Load("Load" + i, "", Load.STATE.IN, r.nextInt(loads));
						inAndStoreLoads.add(load);
					}
					for(int i=0; i<storeLoads; i++){
						load = new Load("StoredLoad" + i, "", Load.STATE.STORE, 0);
						inAndStoreLoads.add(load);
					}
					out = true;
					break;
				
				// Only in is true
				case 1:
					for(int i=0; i<loads; i++){
						load = new Load("Load" + i, "", Load.STATE.IN, r.nextInt(loads));
						inAndStoreLoads.add(load);
					}
					break;
				
				// Only out is true
				case 2:
					throw new LoadListException("Only Out going loads in an empty system is not possible");
				
				// In and out are true
				case 3:
					for(int i=0; i<loads; i++){
						load = new Load("Load" + i, "", Load.STATE.IN, r.nextInt(loads));
						inAndStoreLoads.add(load);
					}
					break;
				
				// Only store is true
				case 4:
					for(int i=0; i<loads; i++){
						load = new Load("StoredLoad" + i, "", Load.STATE.STORE, 0);
						inAndStoreLoads.add(load);
					}
					break;
				
				// store and in are true
				case 5:
					for(int i=0; i<inLoads; i++){
						load = new Load("Load" + i, "", Load.STATE.IN, r.nextInt(loads));
						inAndStoreLoads.add(load);
					}
					for(int i=0; i<storeLoads; i++){
						load = new Load("StoredLoad" + i, "", Load.STATE.STORE, 0);
						inAndStoreLoads.add(load);
					}
					break;
				
				// store and out are true
				case 6:
					for(int i=0; i<loads; i++){
						load = new Load("Load" + i, "", Load.STATE.STORE, 0);
						inAndStoreLoads.add(load);
					}
					break;
				
				// all booleans are true
				case 7:
					for(int i=0; i<inLoads; i++){
						load = new Load("Load" + i, "", Load.STATE.IN, r.nextInt(loads));
						inAndStoreLoads.add(load);
					}
					for(int i=0; i<storeLoads; i++){
						load = new Load("StoredLoad" + i, "", Load.STATE.STORE, 0);
						inAndStoreLoads.add(load);
					}
					break;
			}
			
			// Create output loads
			if(out){
				List<Integer> unusedIndices = new ArrayList<Integer>();
				int randomIndex = 0;
				int systemEntryTime = 0;
				
				for(int i=0; i<loads; i++){
					unusedIndices.add(i);
				}
				
				for(int i=0; i<outLoads; i++){
					// Choose random value
					randomIndex = r.nextInt(unusedIndices.size());
					// Search for the chosen entry
					int randomValue = unusedIndices.get(randomIndex);
					unusedIndices.remove(randomIndex);
					load  = inAndStoreLoads.get(randomValue);
					systemEntryTime = r.nextInt(loads);
					if(systemEntryTime <= load.getEntryTime()){
						systemEntryTime = load.getEntryTime() + 1;
					}
					load = new Load(load.getName(), load.getClassOfGoods(), Load.STATE.OUT, systemEntryTime);
					inAndStoreLoads.add(load);
				}
			}
			this.alGeneratedLoads.addAll(inAndStoreLoads);
			System.out.println();
		}
		else{
			throw new LoadListException("not supported combinatin of load creation parameters"+ " in:"+in+" out:"+out+" store"+store);
		}
	}
	
	
	/**
	 * Add all loads of a list for the scheduling process
	 * 
	 * @param loads List of loads
	 */
	public void addManualLoads(List<Load> loads){
		this.alGeneratedLoads.addAll(loads);
	}
	
	
	/**
	 * Convert a list of normal positions (x,y,z) into java3d positions (x,z,y)
	 * 
	 * @param listToConvert List of positions
	 * @return List of java3d positions
	 */
	public List<Position> convertTo3D(List<Position> listToConvert){
		List<Position> newPositions3D = new ArrayList<Position>();
		for (Position position : listToConvert) {
			newPositions3D.add(new Position(position.getX(), position.getZ(), position.getY()));
		}
		return newPositions3D;
	}


	/**
	 * Convert normal position (x,y,z) into a java3d position (x,z,y)
	 * 
	 * @param positionToConvert Normal position
	 * @return Java3d position
	 */
	public Position convertTo3D(Position positionToConvert){
		 return new Position(positionToConvert.getX(), positionToConvert.getZ(), positionToConvert.getY());
	}


	/**
	 * Get the conveyor
	 * 
	 * @return The conveyor
	 */
	public Conveyor getCon() {
		return this.con;
	}


	/**
	 * Get all buffers
	 * 
	 * @return List of all buffers
	 */
	public List<Buffer> getBuffers() {
		return this.buffers;
	}


	/**
	 * Get all lifts
	 * 
	 * @return List of all lifts
	 */
	public List<Lift> getLifts() {
		return this.lifts;
	}


	/**
	 * Get all created racks
	 * 
	 * @return List of all racks
	 */
	public List<Rack> getRacks() {
		return this.racks;
	}


	/**
	 * Get the list of the loads which are to schedule
	 * 
	 * @return List of loads
	 */
	public List<Load> getScheduledLoads() {
		return this.alScheduledLoads;
	}


	/**
	 * Logical work flow of a new system
	 * 
	 * @param args 
	 */
	public static void main(String[] args){
		Prop p = new Prop();
		//System
		p.setProperty(Prop.LIFT_NUMBER, "2");
		p.setProperty(Prop.RACK_LENGTH, "3");
		p.setProperty(Prop.RACK_HEIGHT, "1");
		//Speeds
		p.setProperty(Prop.LIFT_INIT_POSITION, "1");
		p.setProperty(Prop.CONVEYOR_SPEED, "1");
		p.setProperty(Prop.LIFT_SPEED, "1");
		p.setProperty(Prop.SIMULATION_SPEED, "1");
		
		try {
			ControlCenter cc = new ControlCenter();
			cc.createRandomStrategy();
			cc.generateRandomLoads(6, true, true, true);
			cc.schedule();
			StatisticsManager statManager = new StatisticsManager(cc);
			statManager.collectData();
			statManager.printData();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}
